package com.sojson.util.kd.kdn.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.sojson.config.exception.TransErrorCode;
import com.sojson.config.exception.TransException;
import com.sojson.constant.Constant;
import com.sojson.result.bean.Status;
import com.sojson.util.StringUtil;
import com.sojson.util.SystemParamBaseUtil;
import com.sojson.util.json.JsonUtil;
import com.sojson.util.kd.kdn.KdnUtilInterface;
import com.sojson.util.kd.kdn.bean.KdnConfig;
import com.sojson.util.kd.kdn.bean.KdnParam;
import com.sojson.util.kd.kdn.bean.KdnResponse;
import com.sojson.util.kd.kdn.bean.child.response.KdnResShippers;
import com.sojson.util.kd.kdn.constant.KdnConstant;
import com.sojson.util.kd.kdn.enums.EKdnApiType;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;

/**
 * 快递鸟测试环境工具类实现类
 * 
 * @author liu
 * @date 2021-04-01
 */
public class KdnTestUtilImpl implements KdnUtilInterface {

    private static KdnTestUtilImpl kdn;

    protected KdnTestUtilImpl() {}

    /**
     * 即时查询接口(此接口用于向快递公司实时查询物流轨迹信息)<BR/>
     * 1.主流快递仅支持申通快递、圆通速递、百世快递、天天快递 4家,不支持 EMS、邮政快递包裹、顺丰速运、韵达速递、京东快递、中通快递;
     * 如需查询所有主流快递公司,请选用在途监控服务;<BR/>
     * 2.并发不能超过 10次/S;<BR/>
     * 3.不能超过 500次/天;<BR/>
     * 
     * @param ShipperCode   R   快递公司编码
     * @param LogisticCode  R   快递单号
     * @param CustomerName  C   顺丰快递传(寄件人/收件人手机号后四位),京东快递传商家编码(由发货方提供,且快递单号必须是通过该商家编码下单获取的)(为其他快递时,可不填或保留字段,不可传值)
     * @param OrderCode     O   订单编号
     * @return 不会返回空
     * @throws Exception
     */
    @Override
    public KdnResponse timelyQuery(KdnParam kdnParam) throws Exception {
        return sendPost(kdnParam, "1002");
    }

    /**
     * 轨迹订阅接口(当物流有跟新的时候,会通知参数中的回调接口,直到运单签收)<BR/>
     * 1.主流快递仅支持申通快递、圆通速递、百世快递、天天快递 4家,不支持 EMS、邮政快递包裹、顺丰速运、韵达速递、京东快递、中通快递;
     * 如需查询所有主流快递公司,请选用在途监控服务;<BR/>
     * 2.接口对订阅的数据不做判断和校验,不校验快递公司编码和单号是否匹配,不判断快递单号是否合法、是否正确、是否重复订阅;<BR/>
     * 3.并发不能超过 30次/S;<BR/>
     * 4.不能超过 100单/天;<BR/>
     * 5.推送时效: 6至24小时内<BR/>
     * 
     * @param ShipperCode           R   快递公司编码
     * @param CustomerName          C   顺丰快递传(寄件人/收件人手机号后四位),京东快递传商家编码(由发货方提供,且快递单号必须是通过该商家编码下单获取的)(为其他快递时,可不填或保留字段,不可传值)
     * @param Receiver.PostCode     C   收件地邮编(ShipperCode为 EMS、YZPY、YZBK 时必填)
     * @param Sender.PostCode       C   发件地邮编(ShipperCode为 EMS、YZPY、YZBK 时必填)
     * @param LogisticCode          R   快递单号
     * @param Receiver.Name         R   收件人
     * @param Receiver.Tel          R   电话(电话与手机,必填一个)
     * @param Receiver.Mobile       R   手机(电话与手机,必填一个)
     * @param Receiver.ProvinceName R   收件省(如广东省,不要缺少“省”;如是直辖市,请直接传北京、上海等;如是自治区,请直接传广西壮族自治区等)
     * @param Receiver.CityName     R   收件市(如深圳市,不要缺少“市;如是市辖区,请直接传北京市、上海市等”)
     * @param Receiver.ExpAreaName  R   收件区/县(如福田区,不要缺少“区”或“县”)
     * @param Receiver.Address      R   收件人详细地址(不用传省市区)
     * @param Sender.Name           R   发件人
     * @param Sender.Tel            R   电话(电话与手机,必填一个)
     * @param Sender.Mobile         R   手机(电话与手机,必填一个)
     * @param Sender.ProvinceName   R   发件省(如广东省,不要缺少“省”;如是直辖市,请直接传北京、上海等;如是自治区,请直接传广西壮族自治区等)
     * @param Sender.CityName       R   发件市(如深圳市,不要缺少“市;如是市辖区,请直接传北京市、上海市等”)
     * @param Sender.ExpAreaName    R   发件区/县(如福田区,不要缺少“区”或“县”)
     * @param Sender.Address        R   发件人详细地址(不用传省市区)
     * @return 不会返回空
     * @throws Exception
     */
    @Override
    public KdnResponse trackSubscription(KdnParam kdnParam) throws Exception {
        return sendPost(kdnParam, "1008");
    }

    /**
     * 单号识别接口<BR/>
     * 1.接口只对快递单号进行识别,返回可能属于的一家或多家快递公司,可能性最高的排名靠前,实际归属哪家快递公司仍需用户判断;<BR/>
     * 2.识别失败,接口返回为空;<BR/>
     * 3.快递公司新上线的号段首次会识别失败;进行轨迹订阅并拉取到轨迹后,单号识别接口学习机制会完善此类单号的识别;<BR/>
     * 4.不能超过 30000 次/天<BR/>
     * 
     * @param LogisticCode  R   快递单号
     * @param kdnParam
     * @return 不会返回空
     * @throws Exception
     */
    @Override
    public KdnResponse orderNoDiscern(KdnParam kdnParam) throws Exception {
        return sendPost(kdnParam, "2002");
    }

    /**
     * 单号校验接口(如果单号识别不了,或者不是所选快递的就抛异常)<BR/>
     * 
     * @param ShipperCode   R   快递公司编码
     * @param LogisticCode  R   快递单号
     * @param kdnParam
     * @throws Exception 
     */
    @Override
    public void orderNoVerify(KdnParam kdnParam) throws Exception {
        KdnResponse orderNoDiscern = orderNoDiscern(kdnParam);
        if (isBlankObject(orderNoDiscern) || !orderNoDiscern.getSuccess()) {
            throw new TransException("快递单号校验失败！");
        }

        List<KdnResShippers> shippers = orderNoDiscern.getShippers();
        if (isBlankObject(shippers) || shippers.size() != 1) {
            throw new TransException("快递单号校验失败！");
        }

        String shipperCode = shippers.get(0).getShipperCode();
        if (isBlankObject(shipperCode) || !shipperCode.equals(kdnParam.getShipperCode())) {
            throw new TransException("快递单号校验失败！");
        }
    }

    /**
     * 发送请求
     * 
     * @param kdnParam      Service传过来的参数
     * @param RequestType   请求接口指令(调用快递鸟的那个接口,传接口代码) 
     * @return
     * @throws Exception
     */
    protected KdnResponse sendPost(KdnParam kdnParam, String requestType) throws Exception {
        KdnResponse jsonToObject = jsonToObject(sendPost(EKdnApiType.DIST.getType(), packParam(kdnParam, requestType)));
        Status cusStatus = jsonToObject.getCusStatus();
        if (cusStatus.getCode() != TransErrorCode.SUCCESS.getCode()) {
            throw new TransException(cusStatus.getMsg());
        }
        return jsonToObject;
    }

    /**
     * 封装统一参数
     * 
     * @param kdnParam      Service传过来的参数
     * @param RequestType   请求接口指令(调用快递鸟的那个接口,传接口代码) 
     * @throws Exception
     */
    private Map<String, String> packParam(KdnParam kdnParam, String requestType) throws Exception {
        String objectToJson = objectToJson(kdnParam);
        KdnConfig config = getConfig();
        // 组装系统级参数
        Map<String, String> params = new HashMap<>(5);
        params.put(KdnConstant.PN_REQUEST_TYPE, requestType);
        params.put(KdnConstant.PN_REQUEST_DATA, urlEncoder(objectToJson));
        params.put(KdnConstant.PN_E_BUSINESS_ID, config.geteBusinessID());
        params.put(KdnConstant.PN_DATA_SIGN, urlEncoder(encrypt(objectToJson, config.getApiKey())));
        params.put(KdnConstant.PN_DATA_TYPE, "2");
        return params;
    }

    /**
     * 电商Sign签名生成
     * 
     * @param content   内容
     * @param keyValue  ApiKey
     * @return
     * @throws Exception
     */
    private String encrypt(String content, String keyValue) throws Exception {
        if (keyValue != null) {
            return base64(md5(content + keyValue));
        }
        return base64(md5(content));
    }

    /**
     * MD5加密
     * 
     * @param str   内容
     * @return
     * @throws Exception
     */
    private String md5(String str) throws Exception {
        MessageDigest md = MessageDigest.getInstance("MD5");
        md.update(str.getBytes(Constant.DEFAULT_ENCODING));
        byte[] result = md.digest();
        StringBuffer sb = new StringBuffer(32);
        for (int i = 0; i < result.length; i++) {
            int val = result[i] & 0xff;
            if (val <= 0xf) {
                sb.append("0");
            }
            sb.append(Integer.toHexString(val));
        }
        return sb.toString().toLowerCase();
    }

    /**
     * base64编码
     * 
     * @param str   内容
     * @return
     * @throws IOException
     */
    private String base64(String str) throws IOException {
        return Base64.encode(str.getBytes(Constant.DEFAULT_ENCODING));
    }

    /**
     * 替换Url特殊字符
     * 
     * @param url   Url
     * @return
     * @throws IOException
     */
    private String urlEncoder(String url) throws IOException {
        return encode(url, Constant.DEFAULT_ENCODING);
    }

    /**
     * 替换Url特殊字符
     * @param <T>
     * 
     * @param url   Url
     * @return
     * @throws IOException
     */
    private KdnResponse jsonToObject(String content) {
        KdnResponse jsonToObject = jsonToObject(content, KdnResponse.class);
        Status status = new Status();
        jsonToObject.setCusStatus(status);

        if (jsonToObject.getSuccess()) {
            status.setCode(TransErrorCode.SUCCESS.getCode());
        } else {
            status.setCode(TransErrorCode.FAIL.getCode());
            status.setMsg(jsonToObject.getReason());
        }
        return jsonToObject;
    }

    /**
     * 向指定 URL 发送POST方法的请求
     * url 发送请求的 URL
     * params 请求的参数集合
     * @return 远程资源的响应结果
     */
    private static String sendPost(String url, Map<String, String> params) {
        OutputStreamWriter out = null;
        BufferedReader in = null;
        StringBuilder result = new StringBuilder();
        try {
            URL realUrl = new URL(url);
            HttpURLConnection conn = (HttpURLConnection)realUrl.openConnection();
            // 发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            // POST方法
            conn.setRequestMethod("POST");
            // 设置通用的请求属性
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            conn.connect();
            // 获取URLConnection对象对应的输出流
            out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
            // 发送请求参数
            if (params != null) {
                StringBuilder param = new StringBuilder();
                for (Map.Entry<String, String> entry : params.entrySet()) {
                    if (param.length() > 0) {
                        param.append("&");
                    }
                    param.append(entry.getKey());
                    param.append("=");
                    param.append(entry.getValue());
                }
                out.write(param.toString());
            }
            // flush输出流的缓冲
            out.flush();
            // 定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 使用finally块来关闭输出流、输入流
        finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
        return result.toString();
    }

    /**
     * 设置用户ID,快递鸟提供,注意保管,不要泄漏<BR/>
     * 请到快递鸟官网申请http://www.kdniao.com/ServiceApply.aspx<BR/>
     * 
     * @return
     * @throws IOException 
     */
    private String getEbusinessId() throws IOException {
        return get(KdnConstant.E_BUSINESS_ID);
    }

    /**
     * 获取API key,快递鸟提供,注意保管,不要泄漏<BR/>
     * 登录快递鸟官网会员中心获取https://www.kdniao.com/UserCenter/v4/UserHome.aspx<BR/>
     * 
     * @return
     * @throws IOException 
     */
    private String getApiKey() throws IOException {
        return get(KdnConstant.API_KEY);
    }

    /**
     * 获取快递鸟配置参数
     * 
     * @return
     * @throws IOException 
     */
    private KdnConfig getConfig() throws IOException {
        KdnConfig kdnConfig = new KdnConfig();
        kdnConfig.seteBusinessID(getEbusinessId());
        kdnConfig.setApiKey(getApiKey());
        return kdnConfig;
    }

    /**
     * 判断一个对象是否是空(空对象)
     * 
     * @param obj
     * @return
     */
    private static boolean isBlankObject(Object obj) {
        return StringUtil.isBlankObject(obj);
    }

    /**
     * 使用特定编码编码将字符串转换为application/x-www-form-urlencodedformat
     * 
     * @param url
     * @param encoding
     * @return
     * @throws IOException
     */
    private String encode(String url, String encoding) throws IOException {
        return StringUtil.encode(url, encoding);
    }

    /**
     * 将对象转换成json字符串(值为空的不解析)
     * 
     * @param data 被转换的对象
     * @return 得到的字符串
     */
    private String objectToJson(Object data) throws IOException {
        return JsonUtil.objectToJson(data);
    }

    /**
     * 将json字符串转换成对象
     * 
     * @param jsonData json字符串
     * @param beanType 要转换成的对象的类型
     * @return 得到的对象
     */
    private <T> T jsonToObject(String jsonData, Class<T> beanType) {
        return JsonUtil.jsonToObject(jsonData, beanType);
    }

    /**
     * 从缓存中获取值
     * 
     * @param key
     * @return
     * @throws IOException
     */
    private String get(String key) throws IOException {
        return SystemParamBaseUtil.get(key);
    }

    public static KdnTestUtilImpl getInstances() {
        if (isBlankObject(kdn)) {
            kdn = new KdnTestUtilImpl();
        }
        return kdn;
    }

}